package jxtend.eclipse.jdt.hook;

import java.lang.annotation.Annotation;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.annotation.processing.Processor;
import javax.annotation.processing.SupportedAnnotationTypes;

import jxtend.processor.eclipse.visitor.handler.impl.JxtendTypeConstants;
import jxtend.processor.reflect.ReflectField;
import jxtend.processor.reflect.ReflectMethod;
import jxtend.processor.reflect.ReflectMethod.ResultMethod3Args;
import jxtend.processor.utils.ReflectUtils;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.apt.core.internal.AnnotationProcessorFactoryLoader;
import org.eclipse.jdt.apt.core.internal.IServiceFactory;
import org.eclipse.jdt.apt.core.internal.util.FactoryPath;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.env.AccessRestriction;
import org.eclipse.jdt.internal.compiler.env.ICompilationUnit;
import org.eclipse.jdt.internal.compiler.lookup.Binding;
import org.eclipse.jdt.internal.compiler.lookup.ClassScope;
import org.eclipse.jdt.internal.compiler.lookup.CompilationUnitScope;
import org.eclipse.jdt.internal.compiler.lookup.LookupEnvironment;
import org.eclipse.jdt.internal.compiler.lookup.MissingTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.PackageBinding;
import org.eclipse.jdt.internal.compiler.lookup.ProblemReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.SourceTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.TagBits;
import org.eclipse.jdt.internal.compiler.util.HashtableOfPackage;
import org.eclipse.jdt.internal.compiler.util.HashtableOfType;
import org.eclipse.jdt.internal.core.JavaElement;
import org.eclipse.jdt.internal.core.JavaProject;

@SuppressWarnings("restriction")
public final class EclipseHookUtils {

	private static final ReflectField<JavaElement, JavaElement> parentOfJavaElement =
		ReflectField.getField(JavaElement.class, JavaElement.class, "parent");

	private static final ReflectField<LookupEnvironment, HashtableOfPackage> knownPackagesOfLookupEnvironment =
			ReflectField.getField(LookupEnvironment.class, HashtableOfPackage.class, "knownPackages");
	private static final ReflectField<PackageBinding, HashtableOfType> knownTypesOfPackageBinding =
			ReflectField.getField(PackageBinding.class, HashtableOfType.class, "knownTypes");
	private static final ReflectField<PackageBinding, HashtableOfPackage> knownPackagesOfPackageBinding =
			ReflectField.getField(PackageBinding.class, HashtableOfPackage.class, "knownPackages");

	private static final ResultMethod3Args<
			ClassScope, SourceTypeBinding, SourceTypeBinding, PackageBinding, AccessRestriction> buildTypeMethod =
		ReflectMethod.method(ClassScope.class, SourceTypeBinding.class, "buildType",
				SourceTypeBinding.class, PackageBinding.class, AccessRestriction.class);
	
	public static JavaProject extractJavaProject(ICompilationUnit sourceUnit) {
		JavaElement parent = parentOfJavaElement.get((JavaElement) sourceUnit);
		while (!(parent instanceof JavaProject)) {
			parent = parentOfJavaElement.get(parent);
		}

		return (JavaProject) parent;
	}

	public static Set<Processor> getJxtendProcessors(JavaProject javaProject) {
		Map<IServiceFactory, FactoryPath.Attributes> java6FactoriesAndAttributesMap =
			AnnotationProcessorFactoryLoader.getLoader().getJava6FactoriesAndAttributesForProject(javaProject);

		Set<Processor> result = new HashSet<Processor>();
		for (IServiceFactory processorServiceFactory : java6FactoriesAndAttributesMap.keySet()) {
			if (!processorServiceFactory.toString().startsWith("jxtend")) {
				continue;
			}

			result.add(createProcessor(processorServiceFactory));
		}

		return result;
	}

	public static <T extends Annotation> Set<Class<T>> getAnnotationClassesOfProcessors(
			Set<Processor> annotationProcessors) {
		Set<Class<T>> result = new HashSet<Class<T>>(); 

		for (Processor processor : annotationProcessors) {
			SupportedAnnotationTypes supportedAnnotationTypesAnnotation =
				processor.getClass().getAnnotation(SupportedAnnotationTypes.class);
			if (supportedAnnotationTypesAnnotation == null) {
				continue;
			}

			String[] annotationClassNames = supportedAnnotationTypesAnnotation.value();
			if (annotationClassNames == null) {
				continue;
			}

			for (String className : annotationClassNames) {
				if (className.equals("*")) {
					continue;
				}

				Class<T> annotationClass = getClassForName(className); 
				result.add(annotationClass);
			}
		}

		return result;
	}

	public static void initCompilationUnitScope(
			CompilationUnitDeclaration compilationUnitDeclaration, LookupEnvironment lookupEnvironment) {
		char[][] packageName = (compilationUnitDeclaration.currentPackage != null)
			? compilationUnitDeclaration.currentPackage.tokens : null;

		PackageBinding packageBinding = (packageName == null)
			? new PackageBinding(lookupEnvironment) : new PackageBinding(packageName, null, lookupEnvironment);

		CompilationUnitScope compilationUnitScope =
			new CompilationUnitScope(compilationUnitDeclaration, lookupEnvironment);
		compilationUnitScope.fPackage = packageBinding;
		compilationUnitScope.topLevelTypes = new SourceTypeBinding[0];


		for (TypeDeclaration typeDeclaration : compilationUnitDeclaration.types) {
			ClassScope classScope = new ClassScope(compilationUnitScope, typeDeclaration);

			buildTypeMethod.invoke(classScope, (SourceTypeBinding) null, packageBinding, (AccessRestriction) null);

//			typeDeclaration.binding = createSourceTypeBinding(typeDeclaration, packageBinding, classScope);
			
//			Constructor<UnresolvedReferenceBinding> unresolvedReferenceBindingConstructor =
//				ReflectUtils.getInaccessibleConstructor(
//						UnresolvedReferenceBinding.class, char[][].class, PackageBinding.class);
//
//			//TODO The PackageBinding below is not correct; for now use the same 'packageName' from 'compilationUnitDeclaration'
//			typeDeclaration.binding.superclass = ReflectUtils.newInstance(unresolvedReferenceBindingConstructor,
//					new char[][] { "java".toCharArray(), "lang".toCharArray(), "Object".toCharArray() },
//					new PackageBinding(
//							new char[][] { "java".toCharArray(), "lang".toCharArray() }, null, lookupEnvironment));

//			//TODO: Investigate if the next line is really needed
//			typeDeclaration.initializerScope = new MethodScope(classScope, typeDeclaration, false);

//			ReflectUtils.invokeInaccessibleNoParameterMethod(typeDeclaration.scope, "connectSuperclass");
			
			ReflectUtils.invokeInaccessibleNoParameterMethod(classScope, "buildFieldsAndMethods");
		}
	}

//	public static void clearCompilationUnitScope(CompilationUnitDeclaration compilationUnitDeclaration) {
//		compilationUnitDeclaration.scope.referenceContext = null;
//		compilationUnitDeclaration.scope = null;
//		compilationUnitDeclaration.types[0].binding = null;
//	}

	public static SourceTypeBinding createSourceTypeBinding(
			TypeDeclaration typeDeclaration, PackageBinding packageBinding, ClassScope classScope) {
		SourceTypeBinding result = new SourceTypeBinding(packageBinding.compoundName, packageBinding, classScope);
		result.memberTypes = Binding.NO_MEMBER_TYPES;
		result.typeVariables = Binding.NO_TYPE_VARIABLES;
		result.scope = classScope;
		return result;
	}

	@SuppressWarnings("unchecked")
	private static <T> Class<T> getClassForName(String className) {
		try {
			return (Class<T>) Class.forName(className);
		} catch (ClassNotFoundException e) {
			throw new JxtendEclipseHookException(e);
		}
	}
	
	private static Processor createProcessor(IServiceFactory processorServiceFactory) {
		try {
			return (Processor) processorServiceFactory.newInstance();
		} catch (CoreException e) {
			throw new JxtendEclipseHookException(e);
		}
	}

	public static void clearMissingKeywordAnnotationTypes(LookupEnvironment lookupEnvironment) {
		HashtableOfPackage knownPackages = knownPackagesOfLookupEnvironment.get(lookupEnvironment);
		
		PackageBinding packageBinding;

		packageBinding =
				findPackageBindingInHashtableOfPackage(knownPackages, new char[][] { JxtendTypeConstants.JXTEND });
		if (packageBinding == null) {
			return;
//		} else {
//			HashtableOfPackage knownPackagesOfJxtendPackage = knownPackagesOfPackageBinding.get(packageBinding);
//			for (int i = 0; i < knownPackagesOfJxtendPackage.keyTable.length; i++) {
//				if (!CharOperation.equals("demo".toCharArray(), knownPackagesOfJxtendPackage.keyTable[i])) {
//					continue;
//				}
//
//				HashtableOfType knownTypesOfJxtendDemoPackage =
//						knownTypesOfPackageBinding.get(knownPackagesOfJxtendPackage.valueTable[i]);
//				for (int j = 0; j < knownTypesOfJxtendDemoPackage.keyTable.length; j++) {
//					if (!CharOperation.equals("SampleBean".toCharArray(), knownTypesOfJxtendDemoPackage.keyTable[j])
//							|| !(knownTypesOfJxtendDemoPackage.valueTable[j] instanceof ProblemReferenceBinding)) {
//						continue;
//					}
//
//					knownTypesOfJxtendDemoPackage.keyTable[j] = null;
//					knownTypesOfJxtendDemoPackage.valueTable[j] = null;
//				}
//			}
//
//			HashtableOfType knownTypesOfJxtendPackage = knownTypesOfPackageBinding.get(packageBinding);
//			for (int i = 0; i < knownTypesOfJxtendPackage.keyTable.length; i++) {
//				if (!CharOperation.equals("demo".toCharArray(), knownTypesOfJxtendPackage.keyTable[i])
//						|| (knownTypesOfJxtendPackage.valueTable[i].tagBits & TagBits.HasMissingType) == 0) {
//					continue;
//				}
//
//				knownTypesOfJxtendPackage.keyTable[i] = null;
//				knownTypesOfJxtendPackage.valueTable[i] = null;
//			}
		}

		clearMissingChildPackageBinding(null, -1, packageBinding, 0, JxtendTypeConstants.JXTEND_ANNOTATION_KEYWORD);
	}

	private static PackageBinding findPackageBindingInHashtableOfPackage(
			HashtableOfPackage knownPackages, char[][] compoundName) {
		for (int i = 0; i < knownPackages.keyTable.length; i++) {
			if (knownPackages.valueTable[i] == null
					|| knownPackages.valueTable[i].compoundName.length != compoundName.length
					|| !CharOperation.equals(compoundName, knownPackages.valueTable[i].compoundName)) {
				continue;
			}

			return knownPackages.valueTable[i];
		}

		return null;
	}

	private static void clearMissingChildPackageBinding(PackageBinding parentBinding, int parentKnownPackagesIndex,
			PackageBinding currentPackageBinding, int currentEndIndex, char[][] packageName) {
		if ((currentPackageBinding.tagBits & TagBits.HasMissingType) != 0 && parentBinding != null) {
			HashtableOfPackage knownPackagesOfParentBinding = knownPackagesOfPackageBinding.get(parentBinding);
			knownPackagesOfParentBinding.keyTable[parentKnownPackagesIndex] = null;
			knownPackagesOfParentBinding.valueTable[parentKnownPackagesIndex] = null;

			HashtableOfType knownTypes = knownTypesOfPackageBinding.get(parentBinding);
			if (knownTypes != null) {
				for (int i = 0; i < knownTypes.keyTable.length; i++) {
					if (!CharOperation.equals(packageName[currentEndIndex], knownTypes.keyTable[i])) {
						continue;
					}

					knownTypes.keyTable[i] = null;
					knownTypes.valueTable[i] = null;
				}
			}
		}

		if (currentEndIndex == (packageName.length - 1)
				&& CharOperation.equals(packageName, currentPackageBinding.compoundName)) {
			HashtableOfType knownTypes = knownTypesOfPackageBinding.get(currentPackageBinding);
			for (int i = 0; i < knownTypes.valueTable.length; i++) {
				if (!(knownTypes.valueTable[i] instanceof MissingTypeBinding)) {
					continue;
				}

				knownTypes.keyTable[i] = null;
				knownTypes.valueTable[i] = null;
			}

			return;
		}

		HashtableOfPackage knownPackages = knownPackagesOfPackageBinding.get(currentPackageBinding);
		for (int i = 0; i < knownPackages.keyTable.length; i++) {
			if (!CharOperation.equals(knownPackages.keyTable[i], packageName[currentEndIndex + 1])) {
				continue;
			}

			clearMissingChildPackageBinding(
					currentPackageBinding, i, knownPackages.valueTable[i], currentEndIndex + 1, packageName);
			return;
		}
	}

	private EclipseHookUtils() {
		// utility class
	}
}
